MISC_PIECEWISE

Overview

The MISC_PIECEWISE function fits a collection of specialized piecewise and physics-inspired models to data using nonlinear least squares optimization. This function leverages SciPy’s curve_fit to estimate model parameters that minimize the sum of squared residuals between the fitted function and observed data.

Piecewise linear models are particularly useful when data exhibits distinct behavioral regimes separated by breakpoints. The function supports two-segment and three-segment linear models with automatic breakpoint detection, where the transition point (or points) between segments is fitted along with the slopes and intercepts. The Heaviside Step Two Level model captures abrupt transitions between two constant values, useful for threshold detection in experimental data.

The function also includes physics-based models from magnetism theory. The Langevin Paramagnetic and Langevin Scaled Field models implement the Langevin function, which describes how magnetic dipoles align under an applied field in classical paramagnetic materials. Named after French physicist Paul Langevin who developed the theory in 1905, the function is defined as:

L(x) = \coth(x) - \frac{1}{x}

This function smoothly transitions from linear behavior at small fields to saturation at large fields, making it valuable for modeling magnetic hysteresis and saturation effects.

For multivariate outputs, the function provides the Planar Surface Two Outputs model (fitting two linear functions simultaneously) and the Linear Exponential Multivariate model (combining linear and exponential components across two output dimensions). These are useful when the dependent variable has multiple components that share the same independent variable.

The optimization uses the Levenberg-Marquardt algorithm by default, with automatic initial parameter estimation to improve convergence. The function returns fitted parameter values along with standard errors derived from the covariance matrix when available.

This example function is provided as-is without any representation of accuracy.

Excel Usage

=MISC_PIECEWISE(xdata, ydata, piecewise_model)
  • xdata (list[list], required): The xdata value
  • ydata (list[list], required): The ydata value
  • piecewise_model (str, required): The piecewise_model value

Returns (list[list]): 2D list [param_names, fitted_values, std_errors], or error string.

Examples

Example 1: Demo case 1

Inputs:

piecewise_model xdata ydata
two_segment_linear_breakpoint 0 1.5
1.5 4.8
3 8.1
3.5 9.2
4.5 10
5.5 10.8
6.5 11.6

Excel formula:

=MISC_PIECEWISE("two_segment_linear_breakpoint", {0;1.5;3;3.5;4.5;5.5;6.5}, {1.5;4.8;8.1;9.2;10;10.8;11.6})

Expected output:

a1 k1 xi k2
1.5 2.2 3.5 0.8
2.083e-15 8.593e-16 2.854e-15 1.664e-15

Example 2: Demo case 2

Inputs:

piecewise_model xdata ydata
three_segment_linear_breakpoint 0 0.5
1.2 1.7
2.4 2.9
3.6 3.44
4.8 3.92
6 4.95
7.2 6.75
8.4 8.55

Excel formula:

=MISC_PIECEWISE("three_segment_linear_breakpoint", {0;1.2;2.4;3.6;4.8;6;7.2;8.4}, {0.5;1.7;2.9;3.44;3.92;4.95;6.75;8.55})

Expected output:

a1 k1 xi1 k2 xi2 k3
0.5 1 2.5 0.4 5.5 1.5
0 0 0 0 0 0

Example 3: Demo case 3

Inputs:

piecewise_model xdata ydata
heaviside_step_two_level 0 2.5
1 2.5
2 2.5
3 5
4 5
5 5

Excel formula:

=MISC_PIECEWISE("heaviside_step_two_level", {0;1;2;3;4;5}, {2.5;2.5;2.5;5;5;5})

Expected output:

A B x1
2.5 5 2.5

Example 4: Demo case 4

Inputs:

piecewise_model xdata ydata
langevin_paramagnetic 0.2 -0.760716189912
1.1 -0.19025145403
2 0.460698113858
3.5 1.282091986942
5 1.675444725654
6.5 1.86019989127

Excel formula:

=MISC_PIECEWISE("langevin_paramagnetic", {0.2;1.1;2;3.5;5;6.5}, {-0.760716189912;-0.19025145403;0.460698113858;1.282091986942;1.675444725654;1.86019989127})

Expected output:

y0 xc C
0.1 1.5 2.2
7.358e-7 0.000001264 6.928e-7

Example 5: Demo case 5

Inputs:

piecewise_model xdata ydata
langevin_scaled_field 0.2 -0.140701683384
1.4 0.145836567552
2.6 0.423906840147
3.8 0.67176890878
5 0.877979751168
6.2 1.041693485784

Excel formula:

=MISC_PIECEWISE("langevin_scaled_field", {0.2;1.4;2.6;3.8;5;6.2}, {-0.140701683384;0.145836567552;0.423906840147;0.67176890878;0.877979751168;1.041693485784})

Expected output:

y0 xc C s
0.05 1 1.8 2.5
0.00000259 0.00001105 0.000005469 0.000006939

Example 6: Demo case 6

Inputs:

piecewise_model xdata ydata
planar_surface_two_outputs 0 1 2
1 1.75 1.5
2 2.5 1
3 3.25 0.5
4 4 0
5 4.75 -0.5

Excel formula:

=MISC_PIECEWISE("planar_surface_two_outputs", {0;1;2;3;4;5}, {1,2;1.75,1.5;2.5,1;3.25,0.5;4,0;4.75,-0.5})

Expected output:

a b c d
1 0.75 2 -0.5
0 0 0 0

Example 7: Demo case 7

Inputs:

piecewise_model xdata ydata
linear_exponential_multivariate 0 0.5 2
0.8 1.3 2.216999356
1.6 2.1 2.492860338
2.4 2.9 2.843547305
3.2 3.7 3.289357351
4 4.5 3.856094251

Excel formula:

=MISC_PIECEWISE("linear_exponential_multivariate", {0;0.8;1.6;2.4;3.2;4}, {0.5,2;1.3,2.216999356;2.1,2.492860338;2.9,2.843547305;3.7,3.289357351;4.5,3.856094251})

Expected output:

a1 a2 b1 b2 c
0.5 1.2 1 0.8 0.3
1.848e-7 0.000001529 7.628e-8 0.000001368 2.961e-7

Python Code

import numpy as np
from scipy.optimize import curve_fit as scipy_curve_fit
import math

def misc_piecewise(xdata, ydata, piecewise_model):
    """
    Fits misc_piecewise models to data using scipy.optimize.curve_fit. See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html for details.

    See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html

    This example function is provided as-is without any representation of accuracy.

    Args:
        xdata (list[list]): The xdata value
        ydata (list[list]): The ydata value
        piecewise_model (str): The piecewise_model value Valid options: Two Segment Linear Breakpoint, Three Segment Linear Breakpoint, Heaviside Step Two Level, Langevin Paramagnetic, Langevin Scaled Field, Planar Surface Two Outputs, Linear Exponential Multivariate.

    Returns:
        list[list]: 2D list [param_names, fitted_values, std_errors], or error string.
    """
    def _validate_data(xdata, ydata):
        """Validate and convert input data to numpy arrays."""

        def _convert_x(arg):
            if not isinstance(arg, list) or len(arg) < 2:
                raise ValueError("xdata: must be a 2D list with at least two rows")
            vals = []
            for i, row in enumerate(arg):
                if not isinstance(row, list) or len(row) == 0:
                    raise ValueError(f"xdata row {i}: must be a non-empty list")
                try:
                    vals.append(float(row[0]))
                except Exception:
                    raise ValueError(f"xdata row {i}: non-numeric value")
            return np.asarray(vals, dtype=np.float64)

        def _convert_y(arg):
            if not isinstance(arg, list) or len(arg) < 2:
                raise ValueError("ydata: must be a 2D list with at least two rows")
            rows = []
            expected_len = None
            for i, row in enumerate(arg):
                if not isinstance(row, list) or len(row) == 0:
                    raise ValueError(f"ydata row {i}: must be a non-empty list")
                try:
                    vals = [float(v) for v in row]
                except Exception:
                    raise ValueError(f"ydata row {i}: non-numeric value")
                if expected_len is None:
                    expected_len = len(vals)
                elif len(vals) != expected_len:
                    raise ValueError("ydata rows must all have the same length")
                rows.append(vals)

            y_arr = np.asarray(rows, dtype=np.float64)
            if y_arr.ndim == 1:
                y_arr = y_arr.reshape(-1, 1)
            return y_arr

        x_arr = _convert_x(xdata)
        y_arr = _convert_y(ydata)

        if x_arr.shape[0] != y_arr.shape[0]:
            raise ValueError("xdata and ydata must have the same number of rows")
        return x_arr, y_arr

    # Model definitions dictionary
    models = {
        'two_segment_linear_breakpoint': {
            'params': ['a1', 'k1', 'xi', 'k2'],
            'model': lambda x, a1, k1, xi, k2: np.where(x < xi, a1 + k1 * x, a1 + k1 * xi + k2 * (x - xi)),
            'guess': lambda xa, ya: (float(np.min(ya)), float(np.polyfit(xa, ya, 1)[0]) if xa.size > 1 else 0.0, float(np.median(xa)), float(np.polyfit(xa, ya, 1)[0]) if xa.size > 1 else 0.0),
        },
        'three_segment_linear_breakpoint': {
            'params': ['a1', 'k1', 'xi1', 'k2', 'xi2', 'k3'],
            'model': lambda x, a1, k1, xi1, k2, xi2, k3: np.where(x < xi1, a1 + k1 * x, np.where(x < xi2, a1 + k1 * xi1 + k2 * (x - xi1), a1 + k1 * xi1 + k2 * (xi2 - xi1) + k3 * (x - xi2))),
            'guess': lambda xa, ya: (float(np.min(ya)), 0.0, float(np.percentile(xa, 33)), 0.0, float(np.percentile(xa, 66)), 0.0),
        },
        'heaviside_step_two_level': {
            'params': ['A', 'B', 'x1'],
            'model': lambda x, A, B, x1: np.where(x < x1, A, B),
            'guess': lambda xa, ya: (float(np.percentile(ya, 75)), float(np.percentile(ya, 25)), float(np.median(xa))),
        },
        'langevin_paramagnetic': {
            'params': ['y0', 'xc', 'C'],
            'model': lambda x, y0, xc, C: y0 + C * (np.where(np.abs(x - xc) > 1e-9, np.cosh(x - xc) / np.sinh(x - xc) - 1.0 / (x - xc), 0.0)),
            'guess': lambda xa, ya: (float(np.min(ya)), float(np.median(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0)),
        },
        'langevin_scaled_field': {
            'params': ['y0', 'xc', 'C', 's'],
            'model': lambda x, y0, xc, C, s: y0 + C * (np.where(np.abs(x - xc) > 1e-9, np.cosh((x - xc) / s) / np.sinh((x - xc) / s) - s / (x - xc), 0.0)),
            'guess': lambda xa, ya: (float(np.min(ya)), float(np.median(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0), 1.0),
        },
        'planar_surface_two_outputs': {
            'params': ['a', 'b', 'c', 'd'],
            'model': lambda x, a, b, c, d: np.column_stack((a + b * x, c + d * x)).reshape(-1, order='F'),
            'guess': lambda xa, ya: (
                float(np.mean(ya[:, 0])),
                0.0,
                float(np.mean(ya[:, 1])),
                0.0,
            ),
            'output_mode': 'flatten_columns',
        },
        'linear_exponential_multivariate': {
            'params': ['a1', 'a2', 'b1', 'b2', 'c'],
            'model': lambda x, a1, a2, b1, b2, c: np.column_stack((a1 + b1 * x, a2 + b2 * np.exp(c * x))).reshape(-1, order='F'),
            'guess': lambda xa, ya: (
                float(np.mean(ya[:, 0])),
                float(np.mean(ya[:, 1])),
                0.0,
                0.1,
                0.1,
            ),
            'output_mode': 'flatten_columns',
        }
    }

    # Validate model parameter
    if piecewise_model not in models:
        return f"Invalid model: {str(piecewise_model)}. Valid models are: {', '.join(models.keys())}"

    model_info = models[piecewise_model]

    # Validate and convert input data
    try:
        x_arr, y_matrix = _validate_data(xdata, ydata)
    except ValueError as e:
        return f"Invalid input: {e}"

    output_mode = model_info.get('output_mode', 'single')
    if output_mode == 'single':
        if y_matrix.shape[1] != 1:
            return "Invalid input: ydata must contain a single column for the selected model"
        y_arr = y_matrix[:, 0]
        guess_data = y_arr
    elif output_mode == 'flatten_columns':
        if y_matrix.shape[1] < 2:
            return "Invalid input: ydata must contain at least two columns for the selected model"
        y_arr = y_matrix.reshape(-1, order='F')
        guess_data = y_matrix
    else:
        return f"Invalid model configuration: unsupported output mode '{output_mode}'"

    # Perform curve fitting
    try:
        p0 = model_info['guess'](x_arr, guess_data)
        bounds = model_info.get('bounds', (-np.inf, np.inf))
        if bounds == (-np.inf, np.inf):
            popt, pcov = scipy_curve_fit(model_info['model'], x_arr, y_arr, p0=p0, maxfev=10000)
        else:
            popt, pcov = scipy_curve_fit(model_info['model'], x_arr, y_arr, p0=p0, bounds=bounds, maxfev=10000)

        fitted_vals = [float(v) for v in popt]
        for v in fitted_vals:
            if math.isnan(v) or math.isinf(v):
                return "Fitting produced invalid numeric values (NaN or inf)."
    except ValueError as e:
        return f"Initial guess error: {e}"
    except Exception as e:
        return f"curve_fit error: {e}"

    # Calculate standard errors
    std_errors = None
    try:
        if pcov is not None and np.isfinite(pcov).all():
            std_errors = [float(v) for v in np.sqrt(np.diag(pcov))]
    except Exception:
        pass

    return [model_info['params'], fitted_vals, std_errors] if std_errors else [model_info['params'], fitted_vals]

Online Calculator